// scaup - Scala up!
// (c) 2008-2010, Normen Müller, normen.mueller@gmail.com
// Original version by Jeremy Cloud (http://github.com/jjmmcc/scala0.cmdline#)
// $Id: CommandLineParser.scala 48 2010-01-13 08:02:13Z normen.mueller $
package scaup.draft.cmdln

import scala.collection.mutable.{HashMap, HashSet, ListBuffer}
import java.io.PrintWriter

/** This class is intended to be subclassed by a concrete implementation
 *  that defines a set of options and implements the "spec" method to define
 *  the command line specification.  A typical implementation might look like:
 * <pre>
 *  object Options extends CommandLineParser {
       val version = Flag("v", "version", "Print version number and quit")
       val help = Flag("?", "help", "Print help and quit")
       val dir = StringOption("d", "dir", "dir", "output directory")
       val filename = NakedArgument("filename", "The name of the file")
       val versionMode = %(version)
       val helpMode = %(help)
       val filenameMode = %(dir?, filename)
       val spec = %%(versionMode, helpMode, filenameMode)
   }
 * </pre> */
abstract class CommandLineParser {
  implicit def toOption(str: String) = Some(str)
  implicit def toVariant(spec: OptionSpec): CommandLineVariant = %(spec)
  implicit def toVariant(nakedArg: NakedArgument): CommandLineVariant = %(nakedArg)
    
  /** Builds a CommandLineVariant */
  def %(optionSpec: OptionSpec, nakedArgs: NakedArgument*) = CommandLineVariant(optionSpec, nakedArgs.toList)

  /** Builds a CommandLineVariant */
  def %(nakedArgs: NakedArgument*) = CommandLineVariant(NoOptions, nakedArgs.toList)
    
  /** Builds a list of CommandLineVariants */
  def %%(variants: CommandLineVariant*) = variants.toList
    
  /** Defines the possible command line options. */
  def spec: List[CommandLineVariant]
    
  /** Returns the spec as a string */
  override def toString = spec.mkString("\n")
    
  /** Parses the given command line arguments according to the specification
   *  privided by the "spec" method.  Returns Left(ValidationFailure) if it 
   *  was not possible to parse one of the command line variants.  Returns 
   *  Right(CommandLineVariant,CommandLine) if a variant was matched. */
   def apply(args: Array[String]): Either[ValidationFailure,(CommandLineVariant,CommandLine)] =
     try {
       val optionMap = this.optionMap
       val cmdLineBuf = parse(args.toList)
       val variantResults = spec map { v => 
         matchVariant(cmdLineBuf, v) match {
           case Left(failure) => Left(failure)
           case Right(cmdLine) => Right((v, cmdLine))
         }
       }
            
       variantResults.find(_.isRight).getOrElse {
         if (args.length == 0)
           Left(NoArgumentsProvided)
         else variantResults.last
       }
     } catch {
       case ex: ValidationException => Left(ex.failure)
     }
    
  def printHelp(cmdName: String): Unit = printHelp(cmdName, new PrintWriter(Console.err, true))
    
  def printHelp(cmdName: String, out: PrintWriter) {
    out.println("USAGE:")
    for (v <- spec)
      out.println("\t" + cmdName + " " + v)
    out.println()
    out.println("OPTIONS:")
    for (opt <- optionList)
      out.println(opt.describe)
  }
    
  private lazy val cache = new {
    val optionMap = new HashMap[String,CLOption]
    val optionSet = new HashSet[CLOption]
    val nakedSet = new HashSet[NakedArgument]
    
    for (v <- spec) {
      collectOptions(v.optionSpec)
      nakedSet ++= v.nakedArgs
    }
        
    private def collectOptions(spec: OptionSpec): Unit = spec match {
      case NoOptions =>
        
      case option: CLOption =>
        optionSet += option
        if (option.shortName.isDefined)
          optionMap("-" + option.shortName.get) = option
        if (option.longName.isDefined)
          optionMap("--" + option.longName.get) = option
        
      case OptionalOptionSpec(base) => collectOptions(base)
        
      case AllOfOptionSpec(elts) => collectOptions(elts)
        
      case OneOfOptionSpec(elts) => collectOptions(elts)
    }
        
    private def collectOptions(specs: Iterable[OptionSpec]): Unit = for (spec <- specs) collectOptions(spec)
  }
    
  /** A Map of short and long names (prefixed with "-" and "--" respectively) to the corresponding option. */
  lazy val optionMap = cache.optionMap.readOnly
    
  /** Returns a list of options used in the command line specification.
   *  Options are sorted alphabettically, but with those that have a short name coming before those that don't */
  lazy val optionList = cache.optionSet.toList.sort { (a,b) =>
    if (a.shortName.isDefined)
      !b.shortName.isDefined || a.shortName.get.compareTo(b.shortName.get) < 0
    else if (b.shortName.isDefined)
      false
    else a.longName.get.compareTo(b.longName.get) < 0
  }
    
  private[cmdln] def parse(args: List[String]): CommandLineBuffer = args match {
    case Nil => new CommandLineBuffer
    
    case CLOption.LongForm(longName, value) :: tail =>
      optionMap.get(longName) match {
        case None => throw new ValidationException(UnexpectedArgument(longName))
        
        case Some(flag: Flag) =>
          if (value.isDefined)
            throw new ValidationException(UnexpectedFlagValue(flag, value.get))
          else parse(tail) += ParsedOption(flag, longName, None)
          
        case Some(option: OptionWithValue) =>
          if (!value.isDefined)
            throw new ValidationException(MissingOptionValue(option))
          else {
            option.checkValue(value.get) match {
              case None => parse(tail) += ParsedOption(option, longName, value)
              
              case Some(failure) => throw new ValidationException(failure)
            }
          }
        }
      
    case CLOption.ShortForm(shortName) :: tail =>
      optionMap.get(shortName) match {
        case None => throw new ValidationException(UnexpectedArgument(shortName))
        
        case Some(flag: Flag) => parse(tail) += ParsedOption(flag, shortName, None)
        
        case Some(option: OptionWithValue) =>
          tail match {
            case Nil | (CLOption.LongForm(_, _) | CLOption.ShortForm(_)) :: _ =>
              throw new ValidationException(MissingOptionValue(option))
              
            case tailHead :: tailTail =>
              parse(tailTail) += ParsedOption(option, shortName, Some(tailHead))
          }
        }
      
    case value :: tail => parse(tail) += value  
  }
  
  private def matchVariant(buf: CommandLineBuffer, variant: CommandLineVariant): Either[ValidationFailure,CommandLine] =
    matchOptionSpec(buf.options, variant.optionSpec) match {
      case Left(failure) => Left(failure)
      
      case Right(matchedOptions) =>
        matchNakedArgs(buf.nakedValues, variant.nakedArgs) match {
          case Left(failure) => Left(failure)
          
          case Right(cmdLine) => Right(
            buf.options.values.foldLeft(cmdLine) { (res, opt) =>
              opt match {
                case ParsedOption(flag: Flag, _, _) => res + flag
                
                case ParsedOption(strOpt: StringOption, _, Some(value)) => res + (strOpt, value)
                
                case ParsedOption(intOpt: IntOption, _, Some(value)) => res + (intOpt, value.toInt)
              }
            })
        }
    }
    
  private def matchOptionSpec(options: Map[CLOption,ParsedOption], optionSpec: OptionSpec): Either[ValidationFailure,Set[CLOption]] =
    optionSpec match {
      case NoOptions => Right(Set.empty)
        
      case option: CLOption =>
        if (options.contains(option))
          Right(Set(option))
        else Left(FailedToMatch(optionSpec))
          
      case OptionalOptionSpec(base) =>
        matchOptionSpec(options, base) match {
          case Left(_) => Right(Set.empty)
          case res @ Right(_) => res
        }
          
      case AllOfOptionSpec(elts) =>
        val start = Right(Set.empty): Either[ValidationFailure,Set[CLOption]]
        elts.foldLeft(start) { (res, elt) => res match {
          case left @ Left(_) => left
          case Right(set) => matchOptionSpec(options, elt) match {
            case left @ Left(_) => left
            case Right(set2) => Right(set ++ set2)
          }
        }}

      case OneOfOptionSpec(elts) =>
        val start = Left(FailedToMatch(optionSpec)): Either[ValidationFailure,Set[CLOption]]
        elts.foldLeft(start) { (res, elt) => res match {
          case right @ Right(_) => right
          case left @ Left(_) => matchOptionSpec(options, elt) match {
            case Left(_) => left
            case right @ Right(_) => right
          }
        }}
    }
    
  private def matchNakedArgs(argValues: List[String], argDefs: List[NakedArgument]): Either[ValidationFailure,CommandLine] =
    argDefs match {
      case Nil => argValues match {
        case Nil => Right(CommandLine.empty)
        case argValue :: tail => Left(UnexpectedArgument(argValue))
      }
        
      case argDef :: argDefsTail => argValues match {
        case Nil =>
          if (argDef.required)
            Left(MissingArgument(argDef))
          else matchNakedArgs(argValues, argDefsTail)
           
        case argValue :: argValuesTail =>
          if (argDef.repeated)
            Right(CommandLine.empty + (argDef, argValues))
          else matchNakedArgs(argValuesTail, argDefsTail) match {
            case left @ Left(_) => left
            case Right(cmdLine) => Right(cmdLine + (argDef, List(argValue)))
          }
      }
    }
    
  private[cmdln] class CommandLineBuffer {
    var options = Map.empty[CLOption,ParsedOption]
    var nakedValues: List[String] = Nil
    
    def +=(parsedOption: ParsedOption) = {
      if (options.contains(parsedOption.option))
        throw new ValidationException(DuplicatedOption(parsedOption.option))
      options += (parsedOption.option -> parsedOption)
      this
    }
    
    def +=(arg: String) = {
      nakedValues = arg :: nakedValues
      this
    }
  }

  private[cmdln] case class ParsedOption(option: CLOption, name: String, value: Option[String])
}

private[cmdln] case class CommandLineVariant(optionSpec: OptionSpec, nakedArgs: List[NakedArgument]) {
  override def toString = {
    val prefix = optionSpec.toString
    val suffix = nakedArgs.mkString(" ")
    
    if (prefix.length == 0)
      suffix
    else if (suffix.length == 0)
      prefix
    else prefix + " " + suffix
  }
}
